package jmine.tec.environment.db;

import static bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType.SCHEMA;
import static jmine.tec.environment.EnvironmentMessages.LOADER_NOT_FOUND_FOR_ENTRY;

import java.io.IOException;
import java.sql.SQLException;
import java.util.ArrayList;
import java.util.Arrays;
import java.util.Collections;
import java.util.Iterator;
import java.util.List;

import jmine.tec.environment.Environment;
import jmine.tec.environment.EnvironmentException;
import jmine.tec.environment.EnvironmentMessages;
import jmine.tec.test.testcase.TestCaseException;

import org.apache.log4j.Logger;
import org.hibernate.EntityMode;
import org.hibernate.SessionFactory;
import org.hibernate.metadata.ClassMetadata;
import org.springframework.beans.factory.BeanFactory;

import bancosys.tec.persist.audit.AuditStatus;
import bancosys.tec.persist.authorization.AuthorizationStatus;
import bancosys.tec.persist.uniquevalue.listener.UniqueGeneratedValueEventListener;
import bancosys.tec.utils.db.descriptor.FlatReferenceDatabaseBuilder.ReferenceDatabaseDescriptionType;
import bancosys.tec.utils.db.descriptor.ReferenceDatabaseEntry;
import bancosys.tec.utils.db.executor.CouldNotExecuteException;
import bancosys.tec.utils.loader.ResourceLoader;
import bancosys.tec.utils.log.LogManager;

/**
 * Implementação de um ambiente de banco de dados.
 * 
 * @author Cesar Olmos
 */
public class DBEnvironment implements Environment {

    private static final Logger LOG = Logger.getLogger(DBEnvironment.class);

    private static List<PostscriptEntryLoader> loaders;

    static {

        try {
            new LogManager(new ResourceLoader(), "jmine-tec-test-log.properties", true);
        } catch (IOException e) {
            throw new TestCaseException(e);
        }

        DBEnvironment.register(new SQLPostscriptEntryLoader());
    }

    /**
     * Registers a postscript entry loader
     * 
     * @param loader loader
     */
    public static void register(PostscriptEntryLoader loader) {
        if (loaders == null) {
            loaders = new ArrayList<PostscriptEntryLoader>();
        }
        loaders.add(loader);
    }

    private DBLoadedServerUtils helper;

    private ReferenceDatabaseDescriptionType loadedType = null;

    private ReferenceDatabaseDescriptionType refdbType;

    private boolean dirty = true;

    private boolean canOptimizeDBLoad = true;

    private boolean refdbLoaded;

    private BeanFactory bf;

    private String environmentKey;

    private String databaseServerType;

    private String[] resourcesBaseReferencia;

    /**
     * Construtor de cópia.
     * 
     * @param other o outro objeto a ser copiado.
     */
    public DBEnvironment(DBEnvironment other) {
        this.helper = other.helper;
        this.refdbType = other.refdbType;
        this.loadedType = other.loadedType;
        this.dirty = other.dirty;
        this.canOptimizeDBLoad = other.canOptimizeDBLoad;
        this.refdbLoaded = other.refdbLoaded;
        this.bf = other.bf;
        this.environmentKey = other.environmentKey;
        this.databaseServerType = other.databaseServerType;
        this.resourcesBaseReferencia = other.resourcesBaseReferencia;
    }

    /**
     * Construtor.
     * 
     * @param bf a beanFactory.
     * @param environmentKey o arquivo de configuração do spring.
     * @param resourcesBaseReferencia os recursos da base de referência.
     * @param databaseServerType o tipo do servidor de base de dados.
     * @param refdbType o tipo da base de referência.
     */
    public DBEnvironment(BeanFactory bf, String environmentKey, String databaseServerType, String[] resourcesBaseReferencia,
            ReferenceDatabaseDescriptionType refdbType) {
        this.bf = bf;
        this.environmentKey = environmentKey;
        this.resourcesBaseReferencia = resourcesBaseReferencia;
        this.refdbType = refdbType;
        this.databaseServerType = databaseServerType;
    }

    /**
     * {@inheritDoc}
     */
    public void restart() {
        if (this.helper == null) {
            throw new IllegalStateException("Environment not started");
        }
        List<ReferenceDatabaseDescriptionType> actions =
                new ArrayList<ReferenceDatabaseDescriptionType>(Arrays.asList(this.refdbType.getActions()));
        Collections.reverse(actions);
        try {
            this.loadRefdbActions(actions);
        } catch (CouldNotExecuteException e) {
            throw new EnvironmentException(EnvironmentMessages.ERROR_EXECUTING_REF_DB.create(), e);
        } catch (SQLException e) {
            throw new EnvironmentException(EnvironmentMessages.ERROR_EXECUTING_REF_DB.create(), e);
        }
        this.dirty = false;

        SessionFactory sf = this.getHelper().getSessionFactory();
        for (Iterator<?> it = sf.getAllClassMetadata().values().iterator(); it.hasNext();) {
            ClassMetadata meta = (ClassMetadata) it.next();
            Class<?> clazz = meta.getMappedClass(EntityMode.POJO);
            if (clazz != null) {
                this.getHelper().getSessionFactory().evict(clazz);
            }
        }
        this.invalidateUniqueGeneratedValueCache();

        this.refdbLoaded = true;
    }

    /**
     * Invalidates the cache of reserved ranges for unique value generation
     */
    private void invalidateUniqueGeneratedValueCache() {
        ((UniqueGeneratedValueEventListener) this.getHelper().getBeanFactory().getBean("uniqueGeneratedValueEventListener"))
                .invalidateReservedRanges();
    }

    /**
     * Efetua a carga da primeira action da lista, executando as demais caso necessário
     * 
     * @param actions actions
     * @throws SQLException SQLException
     * @throws CouldNotExecuteException CouldNotExecuteException
     */
    private void loadRefdbActions(List<ReferenceDatabaseDescriptionType> actions) throws SQLException, CouldNotExecuteException {
        if (actions.isEmpty()) {
            this.getHelper().dropTables();
            return;
        }
        ReferenceDatabaseDescriptionType action = actions.get(0);
        if (!this.isDirty() && action.equals(this.loadedType)) {
            return;
        }
        if (SCHEMA.equals(action)) {
            if (SCHEMA.equals(this.loadedType)) {
                this.getHelper().clearDBData();
            } else if (!this.getHelper().restore(action)) {
                this.getHelper().dropTables();
                this.getHelper().createDBSchema();
                this.getHelper().save(SCHEMA);
            }
        } else if (!this.getHelper().restore(action)) {
            this.loadRefdbActions(actions.subList(1, actions.size()));
            this.initializeData(action);
            this.getHelper().save(action);
        }
        this.loadedType = action;
    }

    /**
     * {@inheritDoc}
     */
    public void start() {
        if (this.helper != null) {
            throw new IllegalStateException("Environment already started");
        }
        this.helper = new DBLoadedServerUtils(this.bf, this.environmentKey, this.databaseServerType, this.resourcesBaseReferencia);
        this.refdbLoaded = false;
        this.restart();

    }

    /**
     * {@inheritDoc}
     */
    public void stop() {
        if (this.helper == null) {
            throw new IllegalStateException("Environment not started");
        }
        this.helper.shutdown();
        this.helper = null;
    }

    /**
     * Devolve o helper.
     * 
     * @return o helper.
     */
    public DBLoadedServerUtils getHelper() {
        return this.helper;
    }

    /**
     * Devolve o refdbType.
     * 
     * @return o refdbType.
     */
    public ReferenceDatabaseDescriptionType getRefdbType() {
        return this.refdbType;
    }

    /**
     * Define o refdbType.
     * 
     * @param refdbType o refdbType a ser definido.
     */
    public void setRefdbType(ReferenceDatabaseDescriptionType refdbType) {
        if (this.refdbType.equals(refdbType)) {
            return;
        }
        this.refdbType = refdbType;
        this.refdbLoaded = false;
    }

    /**
     * Devolve o canOptimizeRestart.
     * 
     * @return o canOptimizeRestart.
     */
    public boolean isDirty() {
        return this.dirty;
    }

    /**
     * Marca o ambiente de base de dados como dirty, causando um restart completo. Deve ser chamado quando um teste efetua commits e deixa
     * alterações na base de dados.
     */
    public void markDirty() {
        this.dirty = true;
    }

    /**
     * Devolve o canOptimizeDBLoad.
     * 
     * @return o canOptimizeDBLoad.
     */
    public boolean canOptimizeDBLoad() {
        return this.canOptimizeDBLoad;
    }

    /**
     * Define o canOptimizeDBLoad.
     * 
     * @param canOptimizeDBLoad o canOptimizeDBLoad a ser definido.
     */
    public void setCanOptimizeDBLoad(boolean canOptimizeDBLoad) {
        this.canOptimizeDBLoad = canOptimizeDBLoad;
    }

    /**
     * Devolve o resourcesBaseReferencia.
     * 
     * @return o resourcesBaseReferencia.
     */
    public String[] getResourcesBaseReferencia() {
        return this.resourcesBaseReferencia;
    }

    /**
     * Devolve o environmentKey.
     * 
     * @return o environmentKey.
     */
    public String getEnvironmentKey() {
        return this.environmentKey;
    }

    /**
     * Devolve o databaseServerType.
     * 
     * @return o databaseServerType.
     */
    public String getDatabaseServerType() {
        return this.databaseServerType;
    }

    /**
     * Constrói a base de referência.
     * 
     * @param action action
     */
    protected void initializeData(ReferenceDatabaseDescriptionType action) {
        AuthorizationStatus previousAuthorizationStatus = this.getHelper().getThreadAuthorizationStatus();
        AuditStatus previousAuditStatus = this.getHelper().getAuditManager().getThreadAuditStatus();

        List<ReferenceDatabaseEntry> entries = this.helper.getStrictRefdb(action);
        try {
            // Desabilitar a autorização e a auditoria para poder montar a base de referencia
            this.getHelper().setThreadAuthorizationStatus(AuthorizationStatus.DISABLED);
            this.getHelper().getAuditManager().setThreadAuditStatus(AuditStatus.DISABLED);

            this.helper.initConnection();
            for (ReferenceDatabaseEntry entry : entries) {
                LOG.info(String.format("[%s] %s", entry.getType(), entry.getClasspathResourcePath()));
                try {
                    this.executa(entry);
                } catch (Throwable t) {
                    throw new EnvironmentException(
                            EnvironmentMessages.ERROR_EXECUTING_REF_DB_ENTRY.create(entry.getClasspathResourcePath()), t);
                }
            }
        } catch (Throwable t) {
            throw new EnvironmentException(EnvironmentMessages.ERROR_EXECUTING_REF_DB.create(), t);
        } finally {
            // Voltar o status original da autorização e da auditoria
            this.getHelper().setThreadAuthorizationStatus(previousAuthorizationStatus);
            this.getHelper().getAuditManager().setThreadAuditStatus(previousAuditStatus);
            try {
                this.helper.closeConnection();
            } catch (SQLException ex) {
                throw new EnvironmentException(EnvironmentMessages.ERROR_EXECUTING_REF_DB.create(), ex);
            }
        }
    }

    /**
     * Executa uma entrada da base de referência.
     * 
     * @param entry a entrada.
     * @throws Throwable caso ocorra algum erro.
     */
    public final void executa(ReferenceDatabaseEntry entry) throws Throwable {
        for (PostscriptEntryLoader loader : loaders) {
            if (loader.load(this, entry)) {
                return;
            }
        }
        throw new EnvironmentException(LOADER_NOT_FOUND_FOR_ENTRY.create(entry.getType(), entry.getClasspathResourcePath()));
    }
}
